<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ERC20 交互工具</title>
  <!-- Vue 3 CDN -->
  <script src="js/vue.global.prod.js"></script>
  <!-- Ethers v5 CDN -->
  <script src="js/ethers.umd.min.js"></script>
  <style>
    :root {
      --bg:#0b0e14; --card:#11151f; --muted:#9aa4b2; --text:#e6eaf2; --accent:#4f8cff; --ok:#20c997; --warn:#ffc107; --err:#ff6b6b;
    }
    * { box-sizing: border-box; }
    body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; background: var(--bg); color: var(--text); }
    a{ color: var(--accent); }
    .wrap { max-width: 1100px; margin: 24px auto; padding: 0 16px; }
    .grid { display:grid; grid-template-columns: repeat(12, 1fr); gap:16px; }
    .card { background: var(--card); border:1px solid #1f2430; border-radius:16px; padding:16px; box-shadow: 0 6px 24px rgba(0,0,0,.25); }
    .row { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
    .row > * { flex: 1 1 auto; }
    .btn { background: var(--accent); color:#fff; border:none; padding:10px 14px; border-radius:12px; cursor:pointer; font-weight:600; }
    .btn:disabled { opacity:.6; cursor:not-allowed; }
    .btn.secondary { background:#2a2f3b; color:var(--text); }
    .btn.warn { background: var(--warn); color:#222; }
    .btn.ok { background: var(--ok); }
    .input, select { width:100%; padding:10px 12px; border-radius:12px; border:1px solid #293140; background:#0f1320; color:var(--text); outline: none; }
    .label { font-size:12px; color: var(--muted); margin-bottom:6px; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
    .kvs { display:grid; grid-template-columns: 200px 1fr; gap:10px; }
    .pill { font-size: 12px; border:1px solid #2a3040; border-radius:999px; padding:4px 10px; color: var(--muted); }
    .footer { text-align:center; color: var(--muted); margin-top: 18px; }
    .title { font-size:20px; font-weight:800; margin:0 0 8px; }
    .subtitle { color: var(--muted); margin-bottom: 12px; }
    .hr { height:1px; background:#1e2433; margin: 10px 0 16px; border:0; }
    .badge-ok{ color: var(--ok); }
    .badge-err{ color: var(--err); }
    code { background:#0f1320; border:1px solid #21283a; padding:2px 6px; border-radius:6px; }
    @media (max-width: 900px) {
      .kvs { grid-template-columns: 1fr; }
    }
  </style>
</head>
<body>
  <div id="app" class="wrap">
    <div class="grid">
      <!-- 连接钱包和合约加载 -->
      <div class="card" style="grid-column: span 12;">
        <div class="row" style="justify-content: space-between; align-items: center; gap:8px">
          <div>
            <div class="title">ERC20 交互工具</div>
          </div>
          <div class="row" style="gap:8px; flex:0 0 auto; align-items: center">
            <span class="pill">网络：<span class="mono">{{ prettyChain(chainId || '未知') }}</span></span>
            <input class="input mono" placeholder="合约地址" v-model.trim="tokenAddress" style="width: 400px;" />
            <button class="btn" @click="loadToken" :disabled="!connected || !tokenAddress">加载合约</button>
            <button class="btn" @click="connect" :disabled="connecting">{{ connected? short(account): (connecting? '连接中…':'连接钱包') }}</button>
            <span class="pill" v-if="tokenMeta.name">已加载：{{ tokenMeta.symbol }}</span>
          </div>
        </div>
        <div class="subtitle" v-if="tokenError">
          <span class="badge-err">⚠</span> {{ tokenError }}
        </div>
      </div>

      <!-- 左侧面板：转账和授权功能 -->
      <div class="card" style="grid-column: span 6;" v-if="tokenMeta.name">
        <div class="title">转账功能</div>
        <div class="kvs mono">
          <div>我的余额</div><div>{{ balances.self }}</div>
        </div>
        <hr class="hr">

        <!-- 转账 -->
        <div style="margin-bottom: 16px;">
          <div class="label">转账 (transfer)</div>
          <div class="row">
            <div style="flex:1">
              <input class="input mono" v-model.trim="tx.to" placeholder="收款地址" />
            </div>
            <div style="flex:0.7">
              <input class="input mono" v-model.trim="tx.amount" placeholder="数量" />
            </div>
            <div style="flex:0 0 auto">
              <button class="btn ok" @click="doTransfer">发送</button>
            </div>
          </div>
          <div class="subtitle" v-if="txHash">
            交易：<a :href="explorerTx(txHash)" target="_blank" rel="noreferrer">{{ short(txHash) }}</a>
          </div>
        </div>

        <!-- 授权 -->
        <div style="margin-bottom: 16px;">
          <div class="label">授权 (approve)</div>
          <div class="row">
            <div style="flex:1">
              <input class="input mono" v-model.trim="appr.spender" placeholder="被授权者地址" />
            </div>
            <div style="flex:0.7">
              <input class="input mono" v-model.trim="appr.amount" placeholder="授权数量" />
            </div>
            <div style="flex:0 0 auto">
              <button class="btn warn" @click="doApprove">授权</button>
            </div>
          </div>
          <div class="subtitle" v-if="apprHash">
            交易：<a :href="explorerTx(apprHash)" target="_blank" rel="noreferrer">{{ short(apprHash) }}</a>
          </div>
        </div>

        <!-- 授权转账功能 -->
        <div>
          <div class="label">授权转账 (transferFrom)</div>
          <div class="subtitle" style="color: var(--muted); margin-bottom: 8px;">
            需要先授权，然后使用授权转账功能
          </div>
          <div class="row">
            <div style="flex:1">
              <input class="input mono" v-model.trim="transferFrom.from" placeholder="发送者地址" />
            </div>
            <div style="flex:1">
              <input class="input mono" v-model.trim="transferFrom.to" placeholder="收款地址" />
            </div>
            <div style="flex:0.7">
              <input class="input mono" v-model.trim="transferFrom.amount" placeholder="数量" />
            </div>
            <div style="flex:0 0 auto">
              <button class="btn secondary" @click="doTransferFrom" :disabled="!transferFrom.from || !transferFrom.to || !transferFrom.amount">授权转账</button>
            </div>
          </div>
          <div class="subtitle" v-if="transferFromHash">
            交易：<a :href="explorerTx(transferFromHash)" target="_blank" rel="noreferrer">{{ short(transferFromHash) }}</a>
          </div>
        </div>
      </div>

      <!-- 右侧面板：代币信息和查询功能 -->
      <div class="card" style="grid-column: span 6;" v-if="tokenMeta.name">
        <div class="title">代币信息</div>
        <div class="kvs mono">
          <div>名称</div><div>{{ tokenMeta.name }}</div>
          <div>符号</div><div>{{ tokenMeta.symbol }}</div>
          <div>精度</div><div>{{ tokenMeta.decimals }}</div>
          <div>总供应量</div><div>{{ tokenMeta.totalSupplyFormatted }}</div>
        </div>
        <hr class="hr">

        <!-- 查询余额 -->
        <div style="margin-bottom: 16px;">
          <div class="label">查询余额</div>
          <div class="row">
            <div style="flex:1">
              <input class="input mono" v-model.trim="viewAddr" placeholder="地址 (0x...)"/>
            </div>
            <div style="flex:0 0 auto">
              <button class="btn secondary" @click="readBalance(viewAddr)">查询</button>
            </div>
          </div>
          <div style="margin-top:6px" class="mono">{{ balances.other }}</div>
        </div>

        <!-- 查询授权额度 -->
        <div>
          <div class="label">查询授权额度</div>
          <div class="row">
            <div style="flex:1">
              <input class="input mono" v-model.trim="allow.owner" placeholder="owner 地址" />
            </div>
            <div style="flex:1">
              <input class="input mono" v-model.trim="allow.spender" placeholder="spender 地址" />
            </div>
            <div style="flex:0 0 auto">
              <button class="btn secondary" @click="readAllowance">查询</button>
            </div>
          </div>
          <div style="margin-top:6px" class="mono">{{ allow.value }}</div>
        </div>
      </div>

      <!-- 日志&提示 -->
      <div class="card" style="grid-column: span 12;">
        <div class="title">日志</div>
        <div class="mono" style="white-space: pre-wrap; line-height:1.5; max-height: 260px; overflow:auto; background:#0f1320; border:1px solid #1f2434; border-radius:12px; padding:10px;">
{{ logs.join('\n') }}
        </div>
      </div>

      <div class="footer card" style="grid-column: span 12;">
        小贴士：
        <ul>
          <li>确保浏览器已安装 MetaMask 并在目标网络上；</li>
          <li>首次使用请点击“连接钱包”；</li>
          <li>金额输入按 <code>{{ tokenMeta.symbol || 'token' }}</code> 的人类可读单位；内部会自动用 <code>decimals</code> 转换。</li>
        </ul>
      </div>
    </div>
  </div>

<script>
const { createApp, reactive } = Vue

const ERC20_ABI = [
  // 只保留最常用的方法
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
  "function balanceOf(address owner) view returns (uint256)",
  "function allowance(address owner, address spender) view returns (uint256)",
  "function approve(address spender, uint256 amount) returns (bool)",
  "function transfer(address to, uint256 amount) returns (bool)",
  "function transferFrom(address from, address to, uint256 amount) returns (bool)",
  // 事件（可选）
  "event Transfer(address indexed from, address indexed to, uint256 value)",
  "event Approval(address indexed owner, address indexed spender, uint256 value)"
]
// 全局变量，避免 Vue 代理冲突
let _provider = null
let _signer = null
let _token = null

createApp({
  data(){
    return {
      account: '',
      chainId: null,
      chainTarget: '0xaa36a7', // 默认 Sepolia
      connected: false,
      connecting: false,

      tokenAddress: '0x5fbdb2315678afecb367f032d93f642f64180aa3',
      tokenMeta: reactive({ name: '', symbol: '', decimals: 18, totalSupply: '0', totalSupplyFormatted: '-' }),
      tokenError: '',

      balances: reactive({ self: '-', other: '-' }),
      viewAddr: '',

      allow: reactive({ owner: '', spender: '', value: '-' }),

      tx: reactive({ to:'0xFDe2E81e2A23286944d7B2da17dcde22a9c78b2d', amount:'1' }),
      appr: reactive({ spender:'', amount:'' }),
      transferFrom: reactive({ from:'', to:'', amount:'' }),

      txHash: '',
      apprHash: '',
      transferFromHash: '',

      logs: []
    }
  },
  mounted(){
    if(window.ethereum){
      this.logs.push('检测到 window.ethereum，等待连接…')
      // 创建 ethers provider（使用全局变量）
      _provider = new ethers.providers.Web3Provider(window.ethereum)
      // 监听账户/网络变更
      window.ethereum.on?.('accountsChanged', (accs)=>{
        this.account = accs?.[0]||'';
        this.connected = !!this.account;
        _signer = _provider.getSigner()
        if(_token) this.refreshBalances()
      })
      window.ethereum.on?.('chainChanged', (cid)=>{
        this.chainId = cid;
        // 重新创建 provider 和 signer
        _provider = new ethers.providers.Web3Provider(window.ethereum)
        _signer = _provider.getSigner()
        if(_token) this.loadToken()
      })
    } else {
      this.logs.push('未检测到 MetaMask：请安装扩展。')
    }
  },
  methods: {
    prettyChain(id){
      const map = { '0x1':'Ethereum', '0x5':'Goerli', '0xaa36a7':'Sepolia', '0x89':'Polygon', '0x38':'BSC', '0xa4b1':'Arbitrum', '0x2105':'Base' }
      return map[id] || String(id)
    },
    short(v){ return v? v.slice(0,6)+'…'+v.slice(-4) : '' },
    log(msg){ this.logs.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`) },

    async connect(){
      try{
        if(!window.ethereum) throw new Error('未检测到 MetaMask')
        this.connecting = true

        // 使用 ethers provider 连接钱包
        _provider = new ethers.providers.Web3Provider(window.ethereum)
        const accounts = await _provider.send('eth_requestAccounts', [])
        this.account = accounts[0]
        _signer = _provider.getSigner()

        // 获取网络信息
        const network = await _provider.getNetwork()
        this.chainId = '0x' + network.chainId.toString(16)

        this.connected = true
        this.log(`已连接 ${this.account} @ chain ${this.chainId}`)
      }catch(err){
        this.log('连接失败：'+(err?.message||err))
      }finally{ this.connecting = false }
    },

  
    async loadToken(){
      this.tokenError = ''
      try{
        if(!this.connected) await this.connect()
        if(!ethers.utils.isAddress(this.tokenAddress)) throw new Error('非法合约地址')

        // 使用 ethers 创建合约对象
        _token = new ethers.Contract(this.tokenAddress, ERC20_ABI, _provider)

        // 并行读取元数据
        const [name, symbol, decimals, totalSupply] = await Promise.all([
          _token.name(),
          _token.symbol(),
          _token.decimals(),
          _token.totalSupply()
        ])

        this.tokenMeta.name = name
        this.tokenMeta.symbol = symbol
        this.tokenMeta.decimals = Number(decimals)
        this.tokenMeta.totalSupply = totalSupply.toString()
        this.tokenMeta.totalSupplyFormatted = ethers.utils.formatUnits(totalSupply, this.tokenMeta.decimals)

        this.log(`已加载 ${symbol} (${name}), decimals=${this.tokenMeta.decimals}`)
        await this.refreshBalances()

        // 监听 Transfer 事件
        _token.on('Transfer', (from, to, value, event) => {
          this.log(`Transfer: ${from} -> ${to} ${ethers.utils.formatUnits(value, this.tokenMeta.decimals)} ${this.tokenMeta.symbol}`)
          // 自己相关时刷新
          if([from?.toLowerCase(), to?.toLowerCase()].includes(this.account?.toLowerCase())){
            this.refreshBalances()
          }
        })
      }catch(err){
        this.tokenError = err?.message || String(err)
        this.log('加载合约失败：'+ this.tokenError)
      }
    },

    async refreshBalances(){
      if(!_token || !this.account) return
      try{
        const bal = await _token.balanceOf(this.account)
        this.balances.self = `${ethers.utils.formatUnits(bal, this.tokenMeta.decimals)} ${this.tokenMeta.symbol}`
      }catch(err){ this.balances.self = '-'; this.log('读取余额失败：'+(err?.message||err)) }
    },

    async readBalance(addr){
      try{
        if(!ethers.utils.isAddress(addr)) throw new Error('地址不合法')
        const bal = await _token.balanceOf(addr)
        this.balances.other = `${ethers.utils.formatUnits(bal, this.tokenMeta.decimals)} ${this.tokenMeta.symbol}`
      }catch(err){ this.balances.other = '-'; this.log('查询他人余额失败：'+(err?.message||err)) }
    },

    async readAllowance(){
      try{
        const { owner, spender } = this.allow
        if(!ethers.utils.isAddress(owner) || !ethers.utils.isAddress(spender)) throw new Error('owner 或 spender 地址不合法')
        const v = await _token.allowance(owner, spender)
        this.allow.value = `${ethers.utils.formatUnits(v, this.tokenMeta.decimals)} ${this.tokenMeta.symbol}`
      }catch(err){ this.allow.value = '-'; this.log('读取 allowance 失败：'+(err?.message||err)) }
    },

    async doTransfer(){
      try{
        if(!_token) throw new Error('请先加载合约')
        if(!ethers.utils.isAddress(this.tx.to)) throw new Error('收款地址不合法')
        const amount = ethers.utils.parseUnits(String(this.tx.amount||'0'), this.tokenMeta.decimals)

        // 使用带签名者的合约对象
        const tokenWithSigner = _token.connect(_signer)
        const t = await tokenWithSigner.transfer(this.tx.to, amount)

        this.txHash = t.hash
        this.log(`提交转账：${this.short(t.hash)}`)

        // 等待交易确认
        const receipt = await t.wait()
        this.log(`转账成功，区块：${receipt.blockNumber}`)

        await this.refreshBalances()
      }catch(err){ this.log('转账失败：'+(err?.message||err)) }
    },

    async doApprove(){
      try{
        if(!_token) throw new Error('请先加载合约')
        if(!ethers.utils.isAddress(this.appr.spender)) throw new Error('spender 地址不合法')
        const amount = ethers.utils.parseUnits(String(this.appr.amount||'0'), this.tokenMeta.decimals)

        // 使用带签名者的合约对象
        const tokenWithSigner = _token.connect(_signer)
        const t = await tokenWithSigner.approve(this.appr.spender, amount)

        this.apprHash = t.hash
        this.log(`提交授权：${this.short(t.hash)}`)

        // 等待交易确认
        const receipt = await t.wait()
        this.log(`授权成功，区块：${receipt.blockNumber}`)
      }catch(err){ this.log('授权失败：'+(err?.message||err)) }
    },

    async doTransferFrom(){
      try{
        if(!_token) throw new Error('请先加载合约')
        const { from, to, amount } = this.transferFrom
        if(!ethers.utils.isAddress(from)) throw new Error('发送者地址不合法')
        if(!ethers.utils.isAddress(to)) throw new Error('收款地址不合法')
        const parsedAmount = ethers.utils.parseUnits(String(amount||'0'), this.tokenMeta.decimals)

        // 使用带签名者的合约对象
        const tokenWithSigner = _token.connect(_signer)
        const t = await tokenWithSigner.transferFrom(from, to, parsedAmount)

        this.transferFromHash = t.hash
        this.log(`提交授权转账：${this.short(t.hash)}`)

        // 等待交易确认
        const receipt = await t.wait()
        this.log(`授权转账成功，区块：${receipt.blockNumber}`)

        // 刷新余额
        await this.refreshBalances()
      }catch(err){ this.log('授权转账失败：'+(err?.message||err)) }
    },

    explorerBase(){
      // 仅覆盖常见网络，其他网络可自己扩展
      const map = {
        '0x1':'https://etherscan.io',
        '0xaa36a7':'https://sepolia.etherscan.io',
        '0x5':'https://goerli.etherscan.io',
        '0x89':'https://polygonscan.com',
        '0x38':'https://bscscan.com',
        '0xa4b1':'https://arbiscan.io',
        '0x2105':'https://basescan.org'
      }
      return map[this.chainId] || 'https://etherscan.io'
    },
    explorerTx(hash){ return this.explorerBase()+ '/tx/' + hash },
    
  }
}).mount('#app')
</script>
</body>
</html>
